package com.yst.sqlbuilder;

import com.yst.lambda.LambdaFuncUtils;
import com.yst.lambda.TableFieldFunc;
import com.yst.util.ClassUtils;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

public class SQLSelect<T> implements ISQLExpression,IAliasExpression,ISQLSelect,IParentedSelect<T> {
    @Override
    public String generateExpression(GenerateParam param) {
        String selectSQL= "SELECT".concat(" ").concat("\r\n");
        String fieldSQL=String.join(",", generateSelectFields(param)).concat("\r\n");
        String tableSQL="FROM ".concat(StringUtils.isEmpty(getAlias())?table.generateExpression(param):table.generateExpression(param)+" AS ".concat(getAlias()));
        String joinSQL=joins.stream().map(it->it.generateExpression(param))
                .collect(Collectors.joining("\r\n","\r\n","\r\n"));
        String whereSQL= "WHERE".concat(where.generateExpression(param).concat("\r\n"));
        String groupSQL= groupBy.generateExpression(param).concat("\r\n");
        String orderBySQL= orderBy.generateExpression(param).concat("\r\n");
        String limitSQL=limit.generateExpression(param);
        if (!joinSQL.trim().isEmpty()) {
            selectSQL = StringUtils.leftPad("",getLevel()*2," ").concat(selectSQL)
                    .concat(StringUtils.leftPad("",(getLevel()+1)*2," ").concat(fieldSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(tableSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(joinSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(whereSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(groupSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(orderBySQL)).trim();
        }
        else {
            selectSQL = StringUtils.leftPad("",getLevel()*2," ").concat(selectSQL)
                    .concat(StringUtils.leftPad("",(getLevel()+1)*2," ").concat(fieldSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(tableSQL)).concat("\r\n")
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(whereSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(groupSQL))
                    .concat(StringUtils.leftPad("",getLevel()*2," ").concat(orderBySQL)).trim();
        }

        if (!limitSQL.trim().isEmpty()){
            selectSQL=selectSQL.concat("\r\n").concat(StringUtils.leftPad("",getLevel()*2," ")).concat(limitSQL).trim();
        }

        return selectSQL.concat("\r\n");
    }
    private Set<String> fetchDefaultFieldList(){
        return table.getFieldList();
    }
    public Set<String> generateSelectFields(GenerateParam param){
        if (selectFieldList.isEmpty()){
            selectFieldList.addAll(fetchDefaultFieldList());
        }
        Set<String> delRepeat=new HashSet<>(selectFieldList);
        disableFieldList.forEach(delRepeat::remove);
        Set<String> result=new HashSet<>();
        delRepeat.forEach((it)->result.add(it));
        result.addAll(selectFieldExpressions.stream().map(it->it.generateExpression(param)).collect(Collectors.toList()));
        List<SQLField> joinSelect=new ArrayList<>();
        for (SQLJoin join:joins){
            joinSelect.addAll(join.getFieldList());
        }
        result.addAll(joinSelect.stream().map(it->it.generateExpression(param)).collect(Collectors.toList()));
        return result;
    }
    private ISQLTable table;
    private String alias;
    public SQLSelect<T> as(String alias){
        this.alias=alias;
        return this;
    }
    @Override
    public String getAlias() {
        return Objects.isNull(alias)?"":alias;
    }
    private ISQLSelect parentSelect=null;
    @Override
    public SQLSelect<T> getParentSelect() {
        if (parentSelect instanceof SQLSelect) {
            return (SQLSelect<T>) parentSelect;
        }
        return null;
    }
    @Override
    public void setParentSelect(ISQLSelect select) {
        parentSelect=select;
    }

    static class SQLTable<T> implements ISQLTable {
        private Object table;
        SQLTable(Class<T> clazz){
            this.table=clazz;
        }
        SQLTable(SQLSelect<T> sqlTable){
            this.table=sqlTable;
        }
        @Override
        public String generateExpression(GenerateParam param) {
            if (Objects.nonNull(table)&&table instanceof Class){
                return getEntitiesConfig().getTableName((Class<?>) table);
            }
            if (table instanceof SQLSelect){
                return "("+ ((SQLSelect)table).generateExpression(param)+")";
            }
            throw new RuntimeException("not support class "+table.getClass().getName());
        }
        private String alias;
        public SQLTable<T> setAlias(String alias){
            this.alias=alias;
            return this;
        }
        @Override
        public String getAlias() {
            return alias;
        }

        @Override
        public Set<String> getFieldList() {
            if (table instanceof Class){
                Map<String,Field> fieldMap=ClassUtils.getClassFields((Class<?>) table);
                if (Objects.nonNull(fieldMap)) {
                    return fieldMap.keySet();
                }
            }
            if (table instanceof SQLSelect){
                return ((SQLSelect)table).fetchDefaultFieldList();
            }
            return null;
        }
    }
    public SQLSelect<T> from(Class<T> table){
        this.table=new SQLTable<T>(table);
        return this;
    }
    public SQLSelect<T> from(SQLSelect<T> table){
        this.table=new SQLTable<>(table);
        return this;
    }
    private final List<TableFieldFunc<?,?>> disableFieldList=new ArrayList<>();
    @SafeVarargs
    protected final void checkAndSetDefaultTable(TableFieldFunc<T, ?>... fields){
        if (Objects.isNull(table)&&fields.length>0){
            String tableClassName=LambdaFuncUtils.getOwnerClassName(fields[0]);
            try {
                Class<T> clazz = (Class<T>) Class.forName(tableClassName);
                table = new SQLTable<T>(clazz);
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    @SafeVarargs
    public final SQLSelect<T> disableSelect(TableFieldFunc<T, ?>... fields){
        disableFieldList.addAll(Arrays.asList(fields));
        checkAndSetDefaultTable(fields);
        return this;
    }
    private final List<String> selectFieldList=new ArrayList<>();
    @SafeVarargs
    public final SQLSelect<T> select(TableFieldFunc<T, ?>... fields){
        List<SQLField<T>> fieldList=Arrays.stream(fields).map((it)->new SQLField<>(this,it)).collect(Collectors.toList());
        selectFieldList.addAll(fieldList.stream().map(SQLField::getRealField).collect(Collectors.toList()));
        checkAndSetDefaultTable(fields);
        return this;
    }
    private final List<ISelectFieldExpression<T>> selectFieldExpressions=new ArrayList<>();
    @SafeVarargs
    public final SQLSelect<T> select(SQLSelectAggField<T>...aggFields){
        for(SQLSelectAggField<T> sqlSelectAggField:aggFields){
            sqlSelectAggField.setTableAlias(this);
        }
        selectFieldExpressions.addAll(Arrays.asList(aggFields));
        return this;
    }
    @SafeVarargs
    public final SQLSelect<T> select(ISelectFieldExpression<T>... fields){
        this.selectFieldExpressions.addAll(Arrays.asList(fields));
        return this;
    }
    private final List<SQLJoin> joins=new ArrayList<>();
    public <J> SQLJoin<T,J> joinRight(Class<J> table){
        SQLJoin<T,J> sqlJoin=new SQLRightJoin<T,J>(this).join(table);
        joins.add(sqlJoin);
        return sqlJoin;
    }
    public <J> SQLJoin<T,J> joinRight(SQLSelect<J> table){
        if (Objects.nonNull(table)){
            table.setParentSelect(this);
        }
        SQLJoin<T,J> sqlJoin=new SQLRightJoin<T,J>(this).join(table);
        joins.add(sqlJoin);
        return sqlJoin;
    }
    public <J> SQLJoin<T,J> join(Class<J> table){
        SQLJoin<T,J> sqlJoin=new SQLLeftJoin<T,J>(this).join(table);
        joins.add(sqlJoin);
        return sqlJoin;
    }
    public <J> SQLJoin<T,J> join(SQLSelect<J> table){
        if (Objects.nonNull(table)){
            table.setParentSelect(this);
        }
        SQLJoin<T,J> sqlJoin=new SQLLeftJoin<T,J>(this).join(table);
        joins.add(sqlJoin);
        return sqlJoin;
    }
    @Getter
    private final SQLOrderBy<T> orderBy=new SQLOrderBy<>(this);
    @Getter
    private final SQLSelectCondition<T> where=new SQLSelectCondition<>(this);
    @Getter
    private final SQLGroupBy<T> groupBy=new SQLGroupBy<>(this);
    @Getter
    private final SQLLimit<T> limit =new SQLLimit<>(this);
}
